昨天,我們終於把網站從本機送到了真正的執行環境。
網站真的跑起來之後,除了能不能正常運作,效能也慢慢進入需要注意的範圍。
Day6 講資料庫約束時,我們曾經用一句話把 Constraint 和 Index 分開:
約束(Constraint)管資料對不對;索引(Index)影響資料查得快不快。
當時只先看了 Index 在做什麼,至於哪些欄位值得加、怎麼設計,特地留到效能優化再回來拆。
今天,就接著把這一塊打開來看。
如果用最白話的方式理解,我覺得 Index 很像一本書的目錄或索引頁。
假設有一本 1000 頁的書,我想找「巧克力甘納許」。
沒有目錄時,只能從大量內容裡慢慢找。
沒有目錄
整本書 → 一頁一頁找 → 找到內容
有目錄
目錄 → 先找到範圍 → 翻到內容
Database 也有類似的問題。
假設現在有一張:
orders
裡面放了很多訂單。
我要找某個使用者的所有訂單:
SELECT *
FROM orders
WHERE user_id = 123;
如果沒有適合這支 Query 的 Index,Database 可能需要檢查大量 Row(資料列),才能找出符合 user_id = 123 的資料。
這時可以替 user_id 建立 Index:
CREATE INDEX idx_orders_user_id
ON orders(user_id);
Database 就多了一套額外的查找結構,可以先縮小搜尋範圍,再找到需要的資料。
所以 Index 可以先理解成:
替資料建立一套額外的查找結構,讓 Database 在適合的 Query 裡,更有效率地找到資料。
不過有 Index,不代表 Database 每次都一定會用它。
到底要不要用、怎麼用,Database 還會自己判斷。
這件事我們留到明天直接看 EXPLAIN ANALYZE。
今天先從 Index Design 最直接的一步開始:
Index 到底該加在哪裡?
假設 orders 長這樣:
orders
id
user_id
status
created_at
total
看到一張 Table,很容易先盯著欄位問:
哪個欄位比較重要?哪個要加 Index?
但 Index 是拿來幫 Query 找資料的。
所以真正要先看的,是系統平常怎麼查:
WHERE user_id = ?
或:
WHERE user_id = ?
AND status = ?
又或是:
WHERE created_at >= ?
這些反覆出現的查詢方式,就是 Query Pattern(查詢模式)。
例如系統經常執行:
SELECT *
FROM orders
WHERE user_id = 123;
那 user_id 才是一個值得開始評估 Index 的欄位。
不是因為:
user_id看起來很重要。
而是因為:
系統真的經常用
user_id找訂單。
所以設計 Index 的第一步,不是先翻 Schema。
而是先看:
Database 平常到底在回答哪些 Query?
那是不是只要常出現在 WHERE 裡,就全部加 Index?
也不是。
假設 orders 有:
user_id
status
user_id 可能有幾十萬種不同值,但 status 可能只有四種:
pending
paid
completed
cancelled
而且這四種狀態不一定平均分布。
假設整張 Table 有 100 萬筆訂單,其中 90% 都已經是 completed。
這時查:
WHERE user_id = 123
可能一下就從 100 萬筆縮到某個使用者的幾十筆。
但查:
WHERE status = 'completed'
卻還會留下大約 90 萬筆資料。
兩個條件都有篩選資料,但縮小範圍的能力差很多。
這就是 Selectivity(選擇性) 可以幫我們理解的事情。
簡單來說,就是:
一個查詢條件能把資料範圍縮小多少。
像 status = 'completed' 這種篩完還剩下大半張 Table 的條件,單獨替它建立 Index 就不一定划算。
所以:
出現在
WHERE裡,只代表它值得被觀察,不代表一定要加 Index。
實際還要一起看資料分布和 Query 怎麼使用它。
現在把 Query 改成:
SELECT *
FROM orders
WHERE user_id = 123
AND status = 'completed';
這支 Query 經常一起使用:
user_id + status
如果這就是系統常見的 Query Pattern,就可以開始考慮 Composite Index(複合索引)。
原本單欄 Index:
CREATE INDEX idx_orders_user_id
ON orders(user_id);
只有一個欄位。
Composite Index 則可以把多個欄位放在同一個 Index 裡:
CREATE INDEX idx_orders_user_status
ON orders(user_id, status);
也就是:
Single-column Index
(user_id)
Composite Index
(user_id, status)
但這裡有一個很容易誤會的地方。
(user_id, status) 並不等於:
user_id 有一份 Index
+
status 也有一份 Index
我們今天先看 PostgreSQL 最常見的 B-tree Composite Index,這時欄位順序就會影響它怎麼被使用。
假設我們建立:
CREATE INDEX idx_orders_user_status
ON orders(user_id, status);
也就是:
(user_id, status)
可以先把它想成一本按照:
姓氏 → 名字
排列的電話簿。
如果我要找:
姓張的人
很好找。
如果我要找:
姓張,而且名字叫小明的人
也很好找。
但如果我只知道:
名字叫小明
就沒辦法直接從「姓氏」這個排序起點快速縮小範圍。
放回 (user_id, status):
WHERE user_id = ?
以及:
WHERE user_id = ?
AND status = ?
都能從最前面的 user_id 開始縮小搜尋範圍。
但如果只有:
WHERE status = ?
這組 Index 通常就沒有前兩種情況那麼直接有效。
這就是 Leftmost Prefix(最左前綴) 最重要的直覺:
Composite Index 裡,排在前面的欄位會影響 Database 能不能有效縮小搜尋範圍。
所以:
(user_id, status)
和:
(status, user_id)
不能直接當成一樣。
順序也不是照 Schema 從左到右抄。
還是得回頭看:
系統真正的 Query Pattern 是什麼?
講到這裡,很容易冒出下一個想法:
那乾脆把常查的欄位和組合都加上 Index,不就好了?
問題是,Index 不是免費的。
建立 Index 之後,Database 不只要保存原本的 Table,還要另外保存 Index。
而且資料改變時,Index 也要跟著更新。
例如:
INSERT INTO orders ...
新增一筆訂單時,不是只有 Table 多一筆資料。
相關 Index 也需要一起維護;UPDATE、DELETE 也可能帶來額外的 Index 維護成本。
所以 Index 本身就是一種 Trade-off(取捨):
好處
特定查詢可能找得更快
代價
需要額外 Storage
INSERT / UPDATE / DELETE 也要維護 Index
這也是為什麼 Index Design 不是:
能加多少就加多少。
而是:
這支 Query 值不值得用額外的空間和寫入成本,換更好的查找效率?
把今天的概念放回一支完整 Query。
假設訂單頁經常需要:
顯示某位使用者已完成的訂單。
SELECT *
FROM orders
WHERE user_id = 123
AND status = 'completed'
ORDER BY created_at DESC;
前面已經知道,這裡反覆出現的 Query Pattern 是:
user_id + status
所以可以先提出一個 Composite Index:
CREATE INDEX idx_orders_user_status
ON orders(user_id, status);
-- ↑
-- user_id 在前,status 接在後面
為什麼不是看到 WHERE 就替兩個欄位各建一份 Index?
因為這裡真正想支援的是:
WHERE user_id = ?
AND status = ?
這組反覆出現的 Query Pattern。
而 (user_id, status) 也保留了從 user_id 開始查找的能力。
到這裡,今天學到的幾件事其實已經一起用上了:
Query Pattern
→ user_id + status
Selectivity
→ 觀察兩個條件各自能縮小多少資料
Composite Index
→ 把經常一起查的欄位放進同一組 Index
Leftmost Prefix
→ 欄位順序不能隨便放
Trade-off
→ 加 Index 也會增加 Storage 與寫入成本
不過這支 Query 還有一行:
ORDER BY created_at DESC;
那是不是乾脆再把 created_at 放進去:
CREATE INDEX idx_orders_user_status_created
ON orders(user_id, status, created_at DESC);
這樣就一定更快?
現在還不能直接下結論。
因為前面做的,其實都是根據 Query Pattern 提出的 Index Design 候選方案。
到底:
(user_id, status)
比較好,還是:
(user_id, status, created_at DESC)
比較好?
甚至 Database 最後到底有沒有用到這份 Index?
這時就不能只看 SQL 了,而是要直接看它實際怎麼執行。
這會用到 EXPLAIN / EXPLAIN ANALYZE,也就是我們明天要看的內容。
假設今天我把 orders 的 Schema 丟給 Coding Agent:
「幫我優化訂單查詢的效能。」
如果只把 Table 結構交給 AI,卻沒有提供實際 Query Pattern,它就只能先從 Schema 推測哪些欄位可能需要 Index,例如:
CREATE INDEX idx_orders_user_id
ON orders(user_id);
CREATE INDEX idx_orders_status
ON orders(status);
CREATE INDEX idx_orders_created_at
ON orders(created_at);
看起來三個常用欄位都有 Index,好像很完整。
但真正回頭看系統的 Query,才發現最常跑的是:
WHERE user_id = ?
AND status = ?
ORDER BY created_at DESC;
這時問題就不再是:
「哪些欄位可以加 Index?」
而是:
「這支 Query 真正需要什麼 Index?」
這也是 Vibe Coding 和專業開發比較容易拉開差距的地方。
AI 很快就能寫出 CREATE INDEX。
真正需要工程判斷的,是哪些 Index 應該留下。
以前想到「效能優化」,我第一個想到的比較像是 Code。
是不是哪段邏輯寫得不好?
是不是演算法還能再優化?
但重新研究 Index 之後,我覺得很神奇的是:
原來可以透過 Database Design 本身,讓資料被找得更有效率。
而且 Index 也不是加上去就結束了。
還要知道系統平常怎麼 Query、哪些條件真的有區分力、Composite Index 怎麼排,以及這份 Index 帶來的成本值不值得。
今天我們已經可以根據 Query,提出一份有理由的 Index Design。
但還差最後一步:
它實際上真的有比較快嗎?
下一篇,就直接打開 EXPLAIN ANALYZE,看看 Database 到底是怎麼跑這支 Query 的。